package top.milkbox.core.config;

import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.filter.SaServletFilter;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.util.SaTokenConsts;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.milkbox.common.enums.CommonStatusCodeEnum;
import top.milkbox.common.pojo.CommonResult;

@Slf4j
@Configuration
public class SaTokenConfiguration implements WebMvcConfigurer {

    // 注册 Sa-Token 拦截器，打开注解式鉴权功能
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册 Sa-Token 拦截器，打开注解式鉴权功能
        registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
    }


    /**
     * 注册 [Sa-Token 全局过滤器]
     * <pre>
     * 可通过如下方式设置过滤器链，详情请参照官方文档：
     * <a href="https://sa-token.cc/doc.html#/up/global-filter?id=%e8%87%aa%e5%ae%9a%e4%b9%89%e8%bf%87%e6%bb%a4%e5%99%a8%e6%89%a7%e8%a1%8c%e9%a1%ba%e5%ba%8f">自定义过滤器执行顺序</a>
     * &#064;Bean
     * public FilterRegistrationBean&lt;SaServletFilter&gt;
     * </pre>
     * <p>
     * 这里的filter拦截器早于interceptor
     * </p>
     */
    @Bean
    public SaServletFilter getSaServletFilter() {
        SaServletFilter saServletFilter = new SaServletFilter();

        // 前置函数：在每次认证函数之前执行（BeforeAuth 不受 includeList 与 excludeList 的限制，所有请求都会进入）
        // 不管什么请求，都先经过这里。obj是拓展字段，目前没有任何意义（1.37.0版本的sa-token）
        saServletFilter.setBeforeAuth(obj -> {

            SaHolder.getResponse()

                    // ---------- 设置跨域响应头 ----------
                    /*
                      对于跨域请求，我们分两种情况
                      1. 不使用浏览器的自动管理cookie功能。也就是说在前后端分离的项目中（小程序，h5，手机应用等）禁用了cookie功能。
                         sa-token的登录令牌（token）需要在前端手动控制存储
                         浏览器无需管理cookie，所以权限比较松，配置参考如下：
                         // 允许所有域
                         .setHeader("Access-Control-Allow-Origin", "*")
                         // 允许所有请求方式
                         .setHeader("Access-Control-Allow-Methods", "*")
                         // 允许所有头
                         .setHeader("Access-Control-Allow-Headers", "*")
                         // 有效时间
                         .setHeader("Access-Control-Max-Age", "3600");

                      2. 如果硬是要用浏览器的自动存储cookie功能，需要一些额外配置，由于浏览器安全策略，必须明确指定允许的域、方法和允许的头。
                         并且需要额外配置Access-Control-Allow-Credentials为true。配置参考如下：
                         // 使用SaHolder.getRequest().getHeader("Origin")获取请求的来源，相当于允许所有的域
                         .setHeader("Access-Control-Allow-Origin", SaHolder.getRequest().getHeader("Origin"))
                         // 允许所有请求方式
                         .setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT")
                         // 允许的header参数
                         .setHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With")
                         // 有效时间
                         .setHeader("Access-Control-Max-Age", "3600")
                         // 允许浏览器发送与保存cookie
                         .setHeader("Access-Control-Allow-Credentials", "true");

                         // 如果前端使用的是axios（其他的请求框架类似）这需要额外配置，允许跨域携带cookie：withCredentials: true
                         // 设置axios
                         const axiosInstance = axios.create({
                           // 设置请求地址
                           baseURL: import.meta.env.VITE_APP_SERVER_URL,
                           // 允许跨域保存与携带cookie
                           withCredentials: true,
                         })
                     */
                    .setHeader("Access-Control-Allow-Origin", SaHolder.getRequest().getHeader("Origin"))
                    // 允许所有请求方式
                    .setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT")
                    // 允许的header参数
                    .setHeader("Access-Control-Allow-Headers", "Content-Type, X-Requested-With")
                    // 有效时间
                    .setHeader("Access-Control-Max-Age", "3600")
                    // 允许浏览器发送与保存cookie
                    .setHeader("Access-Control-Allow-Credentials", "true");

            // 如果是预检请求，则立即返回到前端
            // 这里的match可以理解为立即匹配请求方法，其参数是可变参数，可以传递多个
            SaRouter.match(SaHttpMethod.OPTIONS)
                    // 当匹配到指定的请求方法后执行free中的函数式接口，参数r代表的是链式写法中操作的对象SaRouterStaff
                    // 可以在函数式接口中抛出StopMatchException异常来立即跳过SaServletFilter过滤器
                    .free(r -> System.out.println("--------OPTIONS预检请求，不做处理"))
                    // 若匹配到则报错BackResultException，若未匹配到则只返回SaRouterStaff对象
                    // 抛出BackResultException异常会由sa-token处理，以text/plain格式将报错信息写入响应体
                    // 而实际上报错的BackResultException中没有任何信息
                    .back();
        });

        // 指定 [拦截路由] 与 [放行路由]
        saServletFilter
                .addInclude("/**")
                .addExclude("/favicon.ico");

        // 认证函数: 每次请求执行
        // 在setBeforeAuth之后，且必须满足addInclude与addExclude的条件时才执行
        // obj是拓展字段，目前没有任何意义（1.37.0版本的sa-token）
        saServletFilter.setAuth(obj -> {
            // 拦截指定的路径后要干的事情...（目前没什么事可干）
            // ...
        });

        // 异常处理函数：每次认证函数发生异常时执行此函数
        // 在setBeforeAuth和setAuth阶段发生异常时执行此函数传递的函数式接口
        // 函数式接口中，e：表示异常对象；返回值将会写入到响应对象中，默认是text/plain格式，可以修改响应的格式来返回json，具体见其源码的注释
        // 此处拦截的异常早于SpringBoot的全局异常拦截器，不受全局异常拦截器的影响
        saServletFilter.setError(e -> {
            String message = StrUtil.format("顶级拦截器（跨域处理）异常，原因：{}", e.getMessage());
            log.error(message, e);
            SaHolder.getResponse().setHeader("Content-Type", SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
            // 由于此处不经过springMVC，所以需要手动转换为json
            return JSONUtil.toJsonStr(
                    new CommonResult<>(CommonStatusCodeEnum.ERROR500.getCode(), message, e.getStackTrace()));
        });

        return saServletFilter;
    }


}
